feat(spec,platform-objects): action param 'visible' predicate; gate create-user phone on features.phoneNumber#2871
Merged
Conversation
…reate-user phone on features.phoneNumber ActionParamSchema gains an optional 'visible' (ExpressionInputSchema / CEL), evaluated against the same scope as action 'visible'. The sys_user create_user action's phoneNumber param is gated on features.phoneNumber == true, so the form only offers a phone field when the opt-in phoneNumber auth plugin is loaded — otherwise admin/create-user rejects the phone. Pairs with objectui's ActionParamDialog honoring param.visible (objectstack-ai/objectui#<pr>). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 96 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Jul 13, 2026
os-zhuang
pushed a commit
that referenced
this pull request
Jul 15, 2026
…params (#2874) Also lists the param-level visible predicate (added in #2871) in the ActionParam field summary, which had drifted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
os-zhuang
added a commit
that referenced
this pull request
Jul 15, 2026
#2874) (#2965) * feat(spec,auth): add public auth feature-flag classification registry + drift guard (#2874 P0) Single registry (PUBLIC_AUTH_FEATURES) classifying all 13 boolean flags served by getPublicConfig() at /auth/config: consumption surface (crud/login/status), default semantics (opt-in '== true' vs default-on '!= false'), and gated spec inputs or an exemption reason. Includes featureGatePredicate + lowerRequiresFeature helpers for the upcoming requiresFeature sugar (P1). The plugin-auth drift guard derives the ACTUAL served key set from a real AuthManager and asserts it equals the registry keys, so a new flag that ships unclassified turns CI red. A semantics check also asserts each flag's registered default matches what getPublicConfig() actually serves - it already caught oidcProvider being default-ON (follows the MCP surface), which issue #2874's own table misclassified as opt-in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx * feat(spec): declarative requiresFeature sugar on actions/params, lowered to visible CEL (#2874 P1) ActionSchema and ActionParamSchema gain an optional requiresFeature: <flag> (z.enum over the registry keys, so a typo fails the parse). A .transform() appended after the schemas' refinements lowers it at parse time into the canonical visible predicate - features.X == true for opt-in flags, features.X != false for default-on (per PUBLIC_AUTH_FEATURES semantics) - AND-composing with any explicit visible ((existing) && gate) and stripping the sugar key from the output, mirroring the normalizeVisibleWhen pattern (ADR-0089). A non-CEL or AST-only visible rejects loudly (ADR-0078) instead of silently dropping the gate. Renderers, lint, and objectui only ever see the canonical visible envelope - the objectui rendering chain needs zero changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx * refactor(platform-objects): migrate all 22 hand-written features.* gates to requiresFeature (#2874 P2a) Mechanical migration of every visible: 'features.X ...' predicate in the identity objects to the declarative requiresFeature annotation, including the compound transfer_ownership gate (residual row predicate stays hand-written; the lowering AND-composes the feature gate onto it). A behavior-equivalence matrix in platform-objects.test.ts pins each lowered visible.source to the exact CEL string that was previously hand-written (parenthesized-composition for the compound row), and asserts the sugar key never survives into parsed objects - so the migration is provably behavior-neutral. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx * feat(spec,platform-objects): gate the audited admin/twoFactor/oidcProvider surfaces + bidirectional completeness guard (#2874 P2b) Audit fixes - capability-dependent actions that previously rendered with the backing plugin off and then 404'd: - requiresFeature: 'admin' on the six sys_user platform-admin actions (ban/unban/unlock/set_user_password/set_user_role/impersonate_user); the stale 'still render but server returns 404' comment is updated. SCIM deployments keep them (SCIM forces features.admin on, ADR-0071). - requiresFeature: 'twoFactor' on the three sys_two_factor actions and the three sys_user self-service 2FA actions (composed onto their existing record.id == ctx.user.id predicates). - requiresFeature: 'oidcProvider' on the five sys_oauth_application actions (composed onto the enable/disable record predicates). feature-gate-guard.test.ts asserts registry<->objects lockstep in both directions: every gatedInputs path resolves to a real input carrying the flag's lowered predicate (removing an annotation goes red), and every features.* reference in a visible predicate is booked in the registry (adding a gate without bookkeeping goes red). Login-surface audit gaps are recorded in the registry and tracked in objectui#2513 (DeviceAuthPage unguarded) and objectui#2514 (passkeys/magicLink advertised but unconsumed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx * chore: changeset for the feature-gate registry + requiresFeature work (#2874) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx * docs(protocol): document requiresFeature capability gates on actions/params (#2874) Also lists the param-level visible predicate (added in #2871) in the ActionParam field summary, which had drifted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx * fix(ci): classify action/requiresFeature in the liveness ledger; avoid reserved word in docs example - Spec property liveness ratchet: requiresFeature is live at parse time (lowered into the already-live visible predicate and stripped), with evidence pointing at lowerRequiresFeature and the #2874 guard tests. - check-role-word (ADR-0090 D3): swap the docs composition example from transfer_ownership to disable_oauth_application so no new use of the reserved word ships. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx * fix(ci): commit api-surface snapshot (9 intentional additions) + regenerated action reference doc - api-surface: the #2874 registry exports (PUBLIC_AUTH_FEATURES et al.) are intentional public API - snapshot updated via gen:api-surface. - references/ui/action.mdx: regenerated. ActionParamSchema now ends in a .transform() (the requiresFeature lowering), which z.toJSONSchema cannot represent on the output side, so the generator drops the ActionParam block - the same pre-existing behavior as ActionSchema and ObjectSchema. The authoritative ActionParam field documentation lives in the hand-written protocol doc (content/docs/protocol/objectui/actions.mdx), updated in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The create-user form offers a Phone Number field that the default backend rejects —
{"code":"invalid_request","message":"Phone numbers require the phoneNumber auth plugin (auth.plugins.phoneNumber)"}— becausephoneNumberis an opt-in better-auth plugin (off by default), yet thesys_usercreate_useraction statically advertises the param. This makes the form follow the plugin: no phone field unless the plugin is loaded.How
ActionParamSchema(spec/ui/action.zod.ts) gains an optionalvisiblepredicate (ExpressionInputSchema/ CEL), evaluated against the same scope as action-levelvisible(current_user/app/data/features). A dialog that honors it omits the param when it's false.sys_user.create_user(platform-objects/identity/sys-user.object.ts) gates itsphoneNumberparam onfeatures.phoneNumber == true.features.phoneNumberis already served in/api/v1/auth/config(getPublicConfig), so the client can evaluate it.Back-compat:
visibleis optional; params without it are unaffected.Verified
ActionParamSchema.safeParse({...visible})parses + preserves the predicate; a param withoutvisiblestill parses.ActionParamDialoghonoringparam.visible) is unit-tested (6 tests: feature-off hides, feature-on shows, absent-flag hides, malformed fails open, and the normalized{dialect,source}served form) with a browser regression showing the create-user dialog renders cleanly.Pairs with
objectui objectstack-ai/objectui#2406 —
ActionParamDialogevaluatesparam.visible. Both must land for the phone field to hide (spec adds the field; objectui honors it).Alternative considered — default-loading the phoneNumber plugin — was rejected: it turns an opt-in auth/sign-in surface (with an SMS-provider dependency for OTP) on for every deployment. "Form follows plugin" is the conservative fix.
🤖 Generated with Claude Code